﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Gurobi;

namespace AuctionDataLayer
{
    public class GurobiCalculations
    {
        DataLayerRepository m_rep = new DataLayerRepository();

        public class WinnerModel
        {
            public int id { get; set; }
            public int amountToPay { get; set; }
        }

        public void DayliCalculations()
        {
            DateTime today = DateTime.Now.AddDays(1);
            IEnumerable<AvailableBidTime> times = m_rep.GetAvailableBidTimes();

            //List of winning id and paying amount
            List<WinnerModel> winners = new List<WinnerModel>();

            //Get the bids for the current date.
            List<Bid> bids = m_rep.GetAllPendingBidsByDate(today).ToList();
            //if there is only one bid for the date calculated
            //TODO update database accor....
            //else

            //Winner bid ids
            var result = CalculationPreparation(today, bids);
            //Winner bids
            var winnerBidsResult = bids.Where(x => result.Contains(x.Id));
            //Sum of winner bids bidding amounts
            int sumOfAllWinningBids = winnerBidsResult.Select(x => x.BiddingAmount).Sum();

            if (result != null)
            {
                //Calculate the Paying amount for all the winner bids
                foreach (var winnerId in result)
                {
                    var tempWinnerBid = bids.Where(x => x.Id == winnerId).SingleOrDefault();
                    if (tempWinnerBid != null)
                    {
                        //var tempResult = CalculationPreparation(today, bids.Where(x => x.GroupId != tempWinnerBid));
                        List<Bid> BidsToBeExcluded = new List<Bid>();
                        foreach (var time in times)
                        {
                            List<Bid> bidsForSameGroupAndTime = bids.Where(x => x.GroupId == tempWinnerBid.GroupId && x.StartTime <= time.Startime && x.EndTime >= time.Endtime).ToList();
                            int tempSize = bidsForSameGroupAndTime.Count();
                            //If there are more than one bid for time slot, create constraint
                            Bid resultBid = bidsForSameGroupAndTime.Where(x => x.Id == tempWinnerBid.Id).FirstOrDefault();
                            if (resultBid != null && tempSize > 1)
                            {
                                foreach (var item in bidsForSameGroupAndTime)
                                {
                                    if (item.Id != tempWinnerBid.Id && !BidsToBeExcluded.Contains(item))
                                    {
                                        BidsToBeExcluded.Add(item);
                                    }
                                }
                            } 
                        }
                        //TODO
                        BidsToBeExcluded.Add(tempWinnerBid);
                        var tempResult = CalculationPreparation( today, bids.Where( x => !BidsToBeExcluded.Contains(x)).ToList() );
                        if (tempResult != null)
                        {
                            //Temp winner bids
                            var tempWinnerBidsResult = bids.Where(x => tempResult.Contains(x.Id));
                            //Temp sum of winner bids bidding amounts
                            int tempSumOfAllWinningBids = tempWinnerBidsResult.Select(x => x.BiddingAmount).Sum();                            
                            int tempPayingAmount = tempWinnerBid.BiddingAmount - (sumOfAllWinningBids - tempSumOfAllWinningBids);

                            winners.Add(new WinnerModel { id = tempWinnerBid.Id, amountToPay = tempPayingAmount });
                            //System.Diagnostics.Trace.WriteLine("To pay: " + tempPayingAmount);
                        }
                    }        
                }


                foreach (var w in winners)
                {
                    System.Diagnostics.Trace.WriteLine("id: " + w.id + " - Amount: " + w.amountToPay);
                    //System.Console.WriteLine("id: " + w.id + " - Amount: " + w.amountToPay);
                }
                //TODO update database: update => declined or accepted                 
                UpdateDatabaseForBids(winners);

                List<Notification> declinedNotfications = new List<Notification>();

                //Get the remaining/declined bids for the current date.                
                List<Bid> remainingBids = m_rep.GetAllPendingBidsByDate(today).Where(x => !ContainWinner(x.Id,winners)).ToList();
                int remainingBidsCtn = remainingBids.Count();
                for (int index = 0; index < remainingBidsCtn; index++)
                {
                    var remainingBid = remainingBids[index];
                    if (remainingBid.AnyRoom == true)
                    {
                        var remainingAnyRoomBids = remainingBids.Where(x => x.AnyRoomId == remainingBid.AnyRoomId && x.Id != remainingBid.Id).ToList();
                        int oldSize = remainingAnyRoomBids.Count();
                        remainingBidsCtn -= oldSize;
                        remainingBids.RemoveAll( x => remainingAnyRoomBids.Exists(y => y.Id == x.Id));
                        //int ctn1 = remainingAnyRoomBids.Count();
                        //for (int i = 0; i < ctn1; i++)
                        //{
                        //    remainingBids.Remove(remainingAnyRoomBids.ElementAt(i));                            
                        //}                      
                        //Remove the rest of the any room bids
                        m_rep.deleteAllBid(remainingAnyRoomBids);                        
                    }

                    IEnumerable<UserInGroup> groupMembersForBid = m_rep.GetMembersOfGroupByIdAll(remainingBid.GroupId);
                    IEnumerable<UserInGroupBid> usersInGroupBidList = m_rep.GetUserInGroupBidById(groupMembersForBid, remainingBid.Id);
                    int membersCount = usersInGroupBidList.Count();

                    //If personaal
                    if (membersCount == 1 && usersInGroupBidList.First().UserInGroup.User.MyGroupId == remainingBid.GroupId)
                    {
                        UserInGroup userInGroup = usersInGroupBidList.First().UserInGroup;
                        UserInGroupBid userInGroupBid = m_rep.GetUserInGroupBidById(userInGroup.Id, remainingBid.AnyRoomId);
                        Group myGroup = m_rep.GetGroupById(userInGroup.GroupId);

                        userInGroup.CreditAddedSpent -= remainingBid.BiddingAmount;
                        //update UserInGroup credit spend
                        m_rep.UpdateUserInGroupBalance(userInGroup);
                        //update group balance
                        m_rep.UpdateGroupBalance(myGroup.Id, myGroup.Balance + remainingBid.BiddingAmount);
                        //update user balance
                        m_rep.UpdateUserBalance(userInGroup.UserId, userInGroup.User.Balance + remainingBid.BiddingAmount);
                        //delete UserInGroupBid
                        m_rep.deleteUserInGroupBid(userInGroupBid);

                        Notification not = new Notification();
                        not.UserId = userInGroup.UserId;
                        not.Message = "Bid declined";
                        not.BidId = remainingBid.Id;
                        not.Active = true;
                        not.Refunded = remainingBid.BiddingAmount;
                        not.Accepted = false;
                        declinedNotfications.Add(not);
                    }
                    //Group Bid
                    else
                    {                        
                        var sumRefound = 0;
                        foreach (var item in usersInGroupBidList)
                        {
                            var realAmount = item.MembersCreditPaid;
                            if (item.UserInGroup.ActiveMember == true)
                            {                                                                
                                sumRefound += item.MembersCreditPaid;
                                var userIngroupItem = groupMembersForBid.Where(x => x.Id == item.UserInGroupId).SingleOrDefault();
                                userIngroupItem.CreditAddedSpent -= realAmount;
                                //update UserInGroup credit spend
                                m_rep.UpdateUserInGroupBalance(userIngroupItem);

                                Notification not = new Notification();
                                not.UserId = item.UserInGroup.UserId;
                                not.Message = "Group bid declined";
                                not.BidId = remainingBid.Id;
                                not.Active = true;
                                not.Refunded = realAmount;
                                not.Accepted = false;
                                declinedNotfications.Add(not);
                            }
                            else
                            {
                                //update group balance
                                m_rep.UpdateGroupBalance(item.UserInGroup.User.MyGroupId, item.UserInGroup.Group.Balance + realAmount);
                                //update user balance
                                m_rep.UpdateUserBalance(item.UserInGroup.UserId, item.UserInGroup.User.Balance + realAmount);

                                Notification not = new Notification();
                                not.UserId = item.UserInGroup.UserId;
                                not.Message = "Group bid declined";
                                not.BidId = remainingBid.Id;
                                not.Active = false;
                                not.Refunded = realAmount;
                                not.Accepted = false;
                                declinedNotfications.Add(not);                           
                            }
                        }
                        //update group balance                        
                        m_rep.UpdateGroupBalance(remainingBid.GroupId, remainingBid.Group.Balance + sumRefound);
                        //delete UserInGroupBidList
                        m_rep.DeleteUserInGroupBids(usersInGroupBidList.ToList());


                        //Important, check if memeber id inactive
                    }

                    //Update bid to declined and set it to be the parent bid
                    remainingBid.AnyRoomId = remainingBid.Id;
                    m_rep.UpdateBidState(remainingBid, 3);
                }

                //add all notifications for declined bids
                m_rep.AddNotifications(declinedNotfications);
            }
        }

        public static bool ContainWinner(int bidId, List<WinnerModel> winners)
        {
            foreach (var item in winners)
            {
                if (bidId == item.id)
                {
                    return true;
                }
            }
            return false;
        }

        public void UpdateDatabaseForBids(List<WinnerModel> winners)
        {             
            //call
            List<Notification> winnerNotfications = new List<Notification>();
            foreach (var winner in winners)
            {
                Bid winnerBid = m_rep.getBidById(winner.id);
                //Update Bids table: single or group bid
                int realBidId = winnerBid.AnyRoomId;
                if (winnerBid.AnyRoom == true)
                {                    
                    var anyRoomRemainingBids = m_rep.getBidByIdForAnyRoom(winnerBid.AnyRoomId, winnerBid.Id);
                    var usersInGroupForBid = m_rep.GetUsersInGroupBidByIdOnly(winnerBid.AnyRoomId);
                    //change the reference of the Bid id to the new winning bid
                    foreach(var item in usersInGroupForBid)
                    {
                        m_rep.UpdateUserInGroupBidId(item, winnerBid);
                    }
                    m_rep.deleteAllBid(anyRoomRemainingBids);
                    m_rep.updateWinnerAnyRoomBid(winnerBid, winner.amountToPay);
                }
                else
                {
                    m_rep.updateWinnerBid(winnerBid, winner.amountToPay);
                }

                var allGroupMembers = m_rep.GetMembersOfGroupByIdAll(winnerBid.GroupId);
                var allRealGroupMembers = m_rep.GetUserInGroupBidById(allGroupMembers, winnerBid.Id);
                int membersCount = allRealGroupMembers.Count();
                
                //If single/personal bid. Check if this is indeed a personal group and not a group containing only one member
                if (membersCount == 1 && allRealGroupMembers.First().UserInGroup.User.MyGroupId == winnerBid.GroupId)
                {
                    UserInGroup userInGroup = allRealGroupMembers.First().UserInGroup;
                    UserInGroupBid userInGroupBid = allRealGroupMembers.First();

                    //Update UserInGroup table
                    int newCreditAddedSpent = userInGroup.CreditAddedSpent - (userInGroupBid.MembersCreditPaid - winner.amountToPay);
                    m_rep.updateWinnerUserInGroup(userInGroup, newCreditAddedSpent);
                    //Update Group table
                    int newBalance = winnerBid.Group.Balance + (userInGroupBid.MembersCreditPaid - winner.amountToPay);
                    m_rep.UpdateGroupBalance(winnerBid.GroupId, newBalance);
                    //Update User Table
                    m_rep.UpdateUserBalance(userInGroup.UserId, newBalance);
                    
                    ////Delete from UserInGroupBid table *******************************
                    //m_rep.DeleteUserInGroupBidById(userInGroupBid.Id);

                    // Update useringroupbid table for keeping track of user´s spent credit                    
                    m_rep.UpdateUserInGroupBidById(userInGroupBid.Id, winner.amountToPay);
                    Notification not = new Notification();
                    not.UserId = userInGroup.UserId;
                    not.Message = "Bid accepted";
                    not.BidId = winnerBid.Id;                    
                    not.Active = true;
                    not.Refunded = userInGroupBid.MembersCreditPaid - winner.amountToPay;
                    not.Accepted = true;
                    winnerNotfications.Add(not);

                }
                else 
                {
                    var allGroupMembersUpdated = new List<UserInGroup>();

                    foreach (var member in allRealGroupMembers)
                    {                        
                        UserInGroup temp = new UserInGroup();
                        temp.UserId = member.UserInGroup.UserId;
                        temp.GroupId = member.UserInGroup.GroupId;
                        temp.CreditAdded = member.MembersCreditPaid;
                        temp.CreditAddedSpent = 0;
                        temp.ActiveMember = member.UserInGroup.ActiveMember;

                        allGroupMembersUpdated.Add(temp);
                    }
                    
                    //Recalculate the new amounts 
                    var calculatedToPay = CalculateAmountToPay.CalculateAmountSubstract(allGroupMembersUpdated, winner.amountToPay);

                    int newGroupBalance = 0;
                    
                    foreach (var item in calculatedToPay)
                    {
                        UserInGroup userInGroup = allGroupMembers.Where(x => x.UserId == item.userId).SingleOrDefault();
                        UserInGroupBid userInGroupBid = allRealGroupMembers.Where(x => x.UserInGroupId == userInGroup.Id).SingleOrDefault();

                        //maybe send nofication on the refound
                        if (userInGroup.ActiveMember == false)
                        {
                            var tempuser = m_rep.GetUserById(item.userId);
                            int newBalance = tempuser.Balance + (userInGroupBid.MembersCreditPaid - item.amountToPay);

                            //Update User Table                            
                            m_rep.UpdateUserBalance(userInGroup.UserId, newBalance);

                            //Update Group table
                            m_rep.UpdateGroupBalance(tempuser.MyGroupId, newBalance);

                            Notification not = new Notification();
                            not.UserId = userInGroup.UserId;
                            not.Message = "Group bid accepted";
                            not.BidId = winnerBid.Id;                            
                            not.Active = false;
                            not.Refunded = userInGroupBid.MembersCreditPaid - item.amountToPay;
                            not.Accepted = true;
                            winnerNotfications.Add(not);
                        }
                        else
                        {
                            //Update UserInGroup table
                            int newCreditAddedSpent = userInGroup.CreditAddedSpent - (userInGroupBid.MembersCreditPaid - item.amountToPay);
                            m_rep.updateWinnerUserInGroup(userInGroup, newCreditAddedSpent);

                            //Sum new spend for active group members
                            newGroupBalance += (userInGroupBid.MembersCreditPaid - item.amountToPay);
                            Notification not = new Notification();
                            not.UserId = userInGroup.UserId;
                            not.Message = "Group bid accepted";
                            not.BidId = winnerBid.Id;
                            not.Active = true;
                            not.Refunded = userInGroupBid.MembersCreditPaid - item.amountToPay;
                            not.Accepted = true;
                            winnerNotfications.Add(not);
                        }
                        // Update useringroupbid table for keeping track of user´s spent credit                    
                        m_rep.UpdateUserInGroupBidById(userInGroupBid.Id, item.amountToPay);
                    }

                    //Update Group table with the sum of all active members new spent money
                    m_rep.UpdateGroupBalance(winnerBid.GroupId, winnerBid.Group.Balance + newGroupBalance);

                    ////Delete UserInGroupBids for all the group memebers *********************************************
                    //m_rep.DeleteUserInGroupBids(allRealGroupMembers.ToList());
                }            
            }
            //add all notifications for accepted bids
            m_rep.AddNotifications(winnerNotfications);

        }


        public List<int> CalculationPreparation(DateTime date, List<Bid> bids)
        {
            try
            {                
                int ctn = 0;
                GRBEnv env = new GRBEnv("gurobiExample.log");
                GRBModel model = new GRBModel(env);
                List<GRBVar> variables = new List<GRBVar>();

                //Get all the rooms available in the system
                IEnumerable<Room> rooms = m_rep.GetAllRooms();
                //Get all the times available in the system
                IEnumerable<AvailableBidTime> times = m_rep.GetAvailableBidTimes();                               

                //Create a Gurobi variable for each bid: name of variable is b + the id.
                int size = bids.Count();
                for (int i = 0; i < size; i++)
                {
                    variables.Add(model.AddVar(0.0, 1.0, 0.0, GRB.BINARY, "b" + bids[i].Id));
                }
                model.Update();

                //Generate constrainsts list: Bid amount x name of the variable
                GRBLinExpr exprSetObjective = 0.0;
                for (int i = 0; i < size; i++)
                {
                    exprSetObjective.AddTerm(bids[i].BiddingAmount, variables[i]);
                }
                //Add the constraints list to the model
                model.SetObjective(exprSetObjective, GRB.MAXIMIZE);
                

                //Create constraints #1: Bid conflict for time unit for each room
                foreach (var room in rooms)
                {
                    foreach (var time in times)
                    {
                        List<Bid> bidsForSameRoomAndTime = bids.Where(x => x.RoomId == room.Id && x.StartTime <= time.Startime && x.EndTime >= time.Endtime).ToList();
                        var tempSize = bidsForSameRoomAndTime.Count();
                        //If there are more than one bid for time slot, create constraint
                        if (tempSize > 1)
                        {
                            GRBLinExpr expr = 0.0;
                            for (int index = 0; index < size; index++)
                            {
                                if (bids[index].RoomId == room.Id && bids[index].StartTime <= time.Startime && bids[index].EndTime >= time.Endtime)
                                {
                                    expr.AddTerm(1.0, variables[index]);
                                }
                            }
                            //Name of constraint: c_ + 1..n
                            model.AddConstr(expr, GRB.LESS_EQUAL, 1.0, "c_" + ctn);
                            ctn++;
                        }
                    }
                }

                //Get all the group ids for all the distinct group
                IEnumerable<int> group = bids.Distinct(new BidComparer()).Select(x => x.GroupId);

                //Create constraints #2: Bid conflict for time unit for each room
                foreach (var id in group)
                {
                    foreach (var time in times)
                    {
                        List<Bid> bidsForSameGroupAndTime = bids.Where(x => x.GroupId == id && x.StartTime <= time.Startime && x.EndTime >= time.Endtime).ToList();
                        var tempSize = bidsForSameGroupAndTime.Count();
                        //If there are more than one bid for time slot, create constraint
                        if (tempSize > 1)
                        {
                            GRBLinExpr expr2 = 0.0;
                            for (int index = 0; index < size; index++)
                            {
                                if (bids[index].GroupId == id && bids[index].StartTime <= time.Startime && bids[index].EndTime >= time.Endtime)
                                {
                                    expr2.AddTerm(1.0, variables[index]);
                                }
                            }
                            model.AddConstr(expr2, GRB.LESS_EQUAL, 1.0, "c_" + ctn);
                            ctn++;
                        }
                    }
                }
                model.Optimize();
                //list of winner bids ids
                List<int> result = new List<int>();
                for (int l = 0; l < variables.Count(); l++)
                {
                    //If winner
                    string winnerId = variables[l].Get(GRB.StringAttr.VarName);
                    if( variables[l].Get(GRB.DoubleAttr.X) == 1 )
                    {
                        result.Add( Convert.ToInt32(winnerId.Substring(1)) );
                    }
                    
                    //System.Diagnostics.Trace.WriteLine(variables[l].Get(GRB.StringAttr.VarName)
                    //                   + " " + variables[l].Get(GRB.DoubleAttr.X));
                }
                //System.Diagnostics.Trace.WriteLine("Obj: " + model.Get(GRB.DoubleAttr.ObjVal));

                //System.Diagnostics.Trace.Write(maximize);
                model.Dispose();
                env.Dispose();
                return result;
            }
            catch (GRBException e)
            {
                //System.Diagnostics.Trace.WriteLine("Error code: " + e.ErrorCode + ". " + e.Message);
                return null;
            }
        }        
    }

    //Helper comparable for selecting bid group id for all distinc group ids
    class BidComparer : IEqualityComparer<Bid>
    {
        public bool Equals(Bid a, Bid b)
        {
            return a.GroupId == b.GroupId;
        }

        public int GetHashCode(Bid a)
        {
            return 0;
        }
    }

}
